1049. Last Stone Weight II
#Algorithm #Algorithm_Knapsack
1. 문제
1-1. 원문
You are given an array of integers stones
where stones[i]
is the weight of the ith
stone.
We are playing a game with the stones. On each turn, we choose any two stones and smash them together. Suppose the stones have weights x
and y
with x <= y
. The result of this smash is:
- If
x == y
, both stones are destroyed, and - If
x != y
, the stone of weightx
is destroyed, and the stone of weighty
has new weighty - x
.
At the end of the game, there is at most one stone left.
Return the smallest possible weight of the left stone. If there are no stones left, return 0
.
Example 1:
Input: stones = [2,7,4,1,8,1]
Output: 1
Explanation:
We can combine 2 and 4 to get 2, so the array converts to [2,7,1,8,1] then,
we can combine 7 and 8 to get 1, so the array converts to [2,1,1,1] then,
we can combine 2 and 1 to get 1, so the array converts to [1,1,1] then,
we can combine 1 and 1 to get 0, so the array converts to [1], then that's the optimal value.
Example 2:
Input: stones = [31,26,33,21,40]
Output: 5
Constraints:
1 <= stones.length <= 30
1 <= stones[i] <= 100
1-2. 내용 번역
stones 배열이 주어지는데, stones[i]
는 i번 인덱스에 있는 돌의 무게이다.
이 돌들을 가지고 게임을 한다. 각 턴에서 두 개의 돌을 맞부딪친다. 두 개의 돌을 각각 x와 y라고 하고 무게는 x <= y 라고 할 때, 게임의 결과는 다음과 같다.
- x의 무게와 y의 무게가 같다면, 두개의 돌은 가루가되서 없어진다.
- x의 무게와 y의 무게가 다르다면 (x < y 라면), 돌 x는 가루가 되서 없어지고 돌 y는 일부 사라지고 y-x 무게를 가진 돌이 된다.
이 게임을 계속 반복해서 최대 남아있는 돌이 하나 이하일 때, 돌의 최소 무게를 구해라. 만약 남아있는 돌이 하나도 없다면 0을 리턴해라.
2. 문제 이해
2-1. 내용 이해
Example1을 가지고 이해해보자.
각 돌들의 무게가 2, 7, 4, 1, 8, 1 이다.
2와 7을 부딪치면 무게2짜리 돌은 사라지고 7짜리 돌은 무게 5를 가진 돌이 된다. 남은 무게 5짜리 돌과 그 다음 돌인 무게 4짜리 돌을 부딪치면 무게 5짜리 돌은 무게 1짜리 돌이 되고, 무게 4짜리 돌은 사라진다. 이런식으로 돌을 맞부딪치는 게임이다. 이렇게 배열 인덱스 순서대로(2 -> 7 -> 4 -> 1 -> 8 -> 1) 진행하게 되면 남는 돌의 무게는 7이 된다.
하지만 2 -> 4 -> 1 -> 1 -> 7 -> 8 순으로 진행하게 되면 남는 돌의 무게는 1이 된다. 어떤 경우도 이 돌들을 가지고 남길 수 있는 가장 최소의 무게는 1이기 때문에 답은 1이 된다.
2-2. 접근법 생각
Knapsack문제만 골라서 풀기 위해 검색해서 찾은 문제이기 때문에 Knapsack으로 풀이법을 구상해보려고 노력했다. 때문에 이 문제를 모든 알고리즘을 대상으로 열어두고 풀이법을 고민해봤을때 Knapsack이 생각이 날 수 있는지는 아직 잘 모르겠다.
얼핏보면 knapsack 알고리즘의 요소들이 잘 보이지 않기 때문인데, 생각을 잠시 하고 난 뒤에는 요소를 독특하게 구성했다는 느낌이 들었다. 보통은 가방의 무게가 주어지거나 적어도 물건의 무게와 가방의 무게는 다른 대상으로 지정되는데 여기서는 돌의 무게를 가방의 무게로 둘 수도 있고 물건의 무게로 둘 수도 있다고 생각해야 knapsack을 구성할 수 있다. 또한 물건의 가치는 돌의 무게가 아니라 가방의 무게와 돌의 무게의 차이이고 가장 적은 가치를 가질 수 있도록 만들어야 한다. 한가지 더 생각하자면 배낭의 무게와 돌의 무게는 뒤바뀔 수도 있다. 그러니까 배낭의 무게가 마이너스가 되어버리면 즉시 돌의 무게와 배낭의 무게가 바뀌어서 배낭의 무게는 항상 양의 값으로 유지시킬 수 있는 것이다.
2-3. 적용한 풀이
Knapsack BottomUp (Tabulation)
배낭 알고리즘 문제를 만나게 되면 처음에는 재귀의 형태로 생각하면서 수학적 귀납법의 식을 만들어내게 되는데, 이것을 계속 전개시키다보면 어느순간 tabulation으로도 생각할 수 있게 된다.
(+ knapsack을 이번에 다시 공부할 때에는 tabulation이 사고에 역행하는 방식이라고 생각되어서 자연적이지 않은 풀이법이라는 생각이 들었는데, knapsack에 익숙해질 수록 tabulation으로 풀게되면서 생각이란건 하면 할 수록 형식을 뛰어넘는구나 라는 생각이 들게된다.)
3. 구현
class Solution {
fun lastStoneWeightII(stones: IntArray): Int {
var total = 0
for (stone in stones) {
total += stone
}
// 배열의 구성은 i번째 돌*배낭의 무게=i번째 돌을 생각할 때 해당 배낭의 무게로 만들 수 있는지의 여부
val memo = Array(stones.size){Array(total+1){false}}
memo[0][stones[0]] = true
for(idx in 1..stones.size-1) {
for(curTotal in 0..total) {
if (memo[idx-1][curTotal] == true) {
// 이번 돌의 무게가 돌의 무게로서 다뤄지는 경우
val diffAbs = Math.abs(curTotal-stones[idx])
// 이번 돌의 무게가 배낭의 무게로 추가되는 경우
val sum = stones[idx]+curTotal
memo[idx][diffAbs] = true
memo[idx][sum] = true
}
}
}
// 마지막 돌에서 만들 수 있는 가장 작은 무게가 얼마인지를 오름차순으로 체크하면서 골라냄
for (total in 0..total) {
if (memo[stones.size-1][total]) {
return total
}
}
return -1
}
}
4. 복잡도
- 시간 복잡도: O(stoneSize*weightTotal)
- 공간 복잡도: stoneSize*weightTotal